<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>手动实现数据双向绑定-Vue(v-model)</title>
	</head>
	<body>
		<div id="app">
			<input type="text" v-model="msg">
			<input type="text" v-model="info.age">
			<h3>{{ msg }}</h3>
			<p>{{ name }}</p>
			<p>{{ name }}</p>
			<p>{{ info.age }}</p>
		</div>
	</body>
	
	<script>
		class Dep {
			constructor() {
				this.subs = {}
			}
			
			addSub(key, watcher) {
				this.subs[key] = this.subs[key] || []
				this.subs[key].push(watcher)
			}
			
			notify(key) {
				this.subs[key] = this.subs[key] || []
				this.subs[key].forEach(watcher => watcher.update())
			}
		}
		
		class Watcher {
			constructor(vm, key, callback) {
				this.vm = vm
				this.key = key
				this.callback = callback
				
				Dep.target = this
				key.split('.').reduce((newObj, key) => newObj[key], vm.$data)
				Dep.target = null
			}
			
			update() {
				const value = this.key.split('.').reduce((newObj, key) => newObj[key], this.vm.$data)
				this.callback(value)
			}
		}
		
		class Vue {
			constructor(options) {
				this.$el = document.querySelector(options.el)
				this.$data = options.data || {}
				setTimeout(() => {
					this.$data.msg = 'asqdsadgsdg'
					this.$data.name = '李四'
					this.$data.info.age = 28
				}, 3000)
				
				this.observer(this.$data)
				this.compile(this.$el, this.$data)
			}
			
			observer(data) {
				const dep = new Dep()
				Object.keys(data).forEach(key => {
					let value = data[key]
					if (Object.prototype.toString.call(value) == '[object Object]') {
						this.observer(value)
					}
					Object.defineProperty(data, key, {
						configurable: true,
						enumerable: true,
						get() {
							Dep.target && dep.addSub(key, Dep.target)
							return value
						},
						set(newval) {
							value = newval
							if (Object.prototype.toString.call(value) == '[object Object]') {
								this.observer(value)
							}
							dep.notify(key)
						}
					})
				})
			}
			
			compile(el, data) {
				const fragment = document.createDocumentFragment()
				let childNode = null
				while(childNode = el.firstChild) {
					fragment.appendChild(childNode)
				}
				
				// 负责对DOM模板进行编译的方法
				const replace = (node) => {
					// 定义匹配差值表达式的正则
					const regMustache = /\{\{\s*(\S+)\s*\}\}/
					//如果是DMO节点 并且是输入框
					if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT'){
						//获取节点的所有属性
						const attrs = Array.from(node.attributes)
						const findResult = attrs.find(attr=>attr.name === 'v-model')
						if (findResult){
							const expStr = findResult.value  
							const value = expStr.split('.').reduce((newObj, key) => newObj[key], data)
							node.value = value
							new Watcher(this, expStr, (newValue) => {
								node.value = newValue
							})
							node.addEventListener('input', (e) => {
								const keyArr = expStr.split('.')
								const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, key) => newObj[key], data)
								obj[keyArr[keyArr.length - 1]] = e.target.value
							})
						}
					}
					// 证明当前的node节点是一个文本子节点，需要进行正则替换
					if (node.nodeType === 3) {
						// 注意:文本子节点，也是一个DOM对象，如果要获取文本子节点的字符串内容，
						// 需要调用textContent属性获取
						const text = node.textContent
						// 进行字符串的正则匹配与提取
						const execResult = regMustache.exec(text)
						if (execResult) {
							const value = execResult[1].split('.').reduce((newObj, key) => newObj[key], data)
							node.textContent = text.replace(regMustache, value)
							new Watcher(this, execResult[1], (newValue) => {
								node.textContent = text.replace(regMustache, newValue)
							})
						}
						// 终止递归条件
						return 
					}
					node.childNodes.forEach(child => replace(child))
				}
				
				// 模板编译
				replace(fragment)
				// 重新渲染页面
				this.$el.appendChild(fragment)
			}
		}
		
		const app = new Vue({
			el: '#app',
			data: {
				msg: '5562',
				name: '法外狂徒-张三',
				info: {
					age: 18
				}
			}
		})
		console.log(app)
	</script>
</html>